//
//  LcJson.m
//  18Birdies
//
//  Created by Lc on 15/3/15.
//  Copyright (c) 2015年 Lc. All rights reserved.
//

#import <objc/runtime.h>

#import "LcJson.h"
#import "LcJsonException.h"
#import "LcPreconditions.h"
#import "LcReflection.h"
#import "LcSecurityCenter.h"
#import "LcSubclassAdapter.h"
#import "LcTypeAdapter.h"
#import "NSString+Lc.h"

@interface LcJson ()
@property(nonatomic, strong) NSMutableDictionary* typeAdapters;
@property(nonatomic, strong) LcSubclassAdapter* defaultSubclassAdapter;
@property(nonatomic, strong) LcSecurityCenter* securityCenter;
@property(nonatomic, strong) NSMutableSet<NSThread*>* jsoningThreads;
@end

@implementation LcJson

#pragma mark - Single Instance

- (void)internalInit {
  self.typeAdapters = [NSMutableDictionary dictionary];
  self.defaultSubclassAdapter =
      [LcSubclassAdapter subclassAdapterWithDefaultSubclassType:nil];
  self.securityCenter = [LcSecurityCenter sharedInstance];
  self.jsoningThreads = [NSMutableSet set];
}

#pragma mark -

- (void)registerTypeAdapter:(id<LcTypeAdapter>)typeAdapter
                    forType:(NSString*)type {
  LC_CHECK_NOT_NULL(typeAdapter);
  LC_CHECK_NOT_EMPTY(type);
  if ([LcReflection isProtocol:type]) {
    NSRange range = NSMakeRange(1, [type length] - 2);
    NSString* protocolName = [type substringWithRange:range];
    Protocol* protocol = NSProtocolFromString(protocolName);
    if (protocol == nil) {
      @throw [LcJsonException
          exceptionWithReason:
              [NSString stringWithFormat:@"protocol not existed, protocol=%@",
                                         protocolName]];
    }
  } else {
    if (![LcReflection isPrimitiveType:type]) {
      Class clazz = NSClassFromString(type);
      if (clazz == NULL) {
        @throw [LcJsonException
            exceptionWithReason:
                [NSString
                    stringWithFormat:@"type not supported, type=%@", type]];
      }
    }
    if ([LcReflection isPrimitiveType:type] ||
        [LcReflection isType:type KindOfClass:NSString.class] ||
        [LcReflection isCollectionType:type]) {
      @throw [LcJsonException
          exceptionWithReason:
              [NSString
                  stringWithFormat:
                      @"primitive types or basic ObjectiveC foundation types "
                      @"cannot register adapter, type=%@",
                      type]];
    }
  }

  self.typeAdapters[type] = typeAdapter;
}

- (void)registerSubclassAdapterForType:(NSString*)type
               withDefaultSubclassType:(NSString*)defaultSubclassType {
  LC_CHECK_NOT_EMPTY(type);
  [self registerTypeAdapter:
            [LcSubclassAdapter
                subclassAdapterWithDefaultSubclassType:defaultSubclassType]
                    forType:type];
}

#pragma mark - Core implementation

- (id)fromJson:(NSData*)data ofClass:(Class)clazz {
  LC_CHECK_NOT_NULL(clazz);
  return [self fromJson:data ofClass:clazz withGenericTypes:nil];
}

- (id)fromJson:(NSData*)data
             ofClass:(Class)clazz
    withGenericTypes:(NSArray*)genericTypes {
  LC_CHECK_NOT_NULL(clazz);
  @try {
    [self startJsoning];

    if (data == nil) {
      return nil;
    }
    if ([LcReflection isType:NSStringFromClass(clazz)
                 KindOfClass:NSString.class]) {
      return [NSMutableString stringWithData:data];
    }
    NSError* error = nil;
    id jsonElement =
    [NSJSONSerialization JSONObjectWithData:data
                                    options:NSJSONReadingMutableContainers |
     NSJSONReadingMutableLeaves |
     NSJSONReadingAllowFragments
                                      error:&error];
    if (error != nil || jsonElement == nil) {
      @throw [LcJsonException
              exceptionWithReason:
              [NSString stringWithFormat:@"NSJSONSerialization deserialization "
               @"exception, error=%@!",
               error.localizedDescription]];
    }
    return [self fromJsonElement:jsonElement
                          toType:NSStringFromClass(clazz)
                withGenericTypes:genericTypes
                 skipTypeAdapter:NO];
  } @finally {
    [self finishJsoning];
  }
}

- (id)fromJsonElement:(id)jsonElement
               toType:(NSString*)type
     withGenericTypes:(NSArray*)genericTypes
      skipTypeAdapter:(BOOL)skipTypeAdapter {
  BOOL isAlreadyJsoning = self.isJsoning;
  @try {
    if (!isAlreadyJsoning) {
      [self startJsoning];
    }
    if (jsonElement == nil) {
      if ([LcReflection isPrimitiveType:type]) {
        return @0;
      } else {
        return nil;
      }
    } else if (jsonElement == [NSNull null]) {
      return [NSNull null];
    }
    
    if (!skipTypeAdapter) {
      id<LcTypeAdapter> typeAdapter = [self typeAdapterForType:type];
      if (typeAdapter != nil) {
        return [typeAdapter deserialize:jsonElement ofType:type withLcJson:self];
      }
    }
    
    id object = nil;
    if ([LcReflection isPrimitiveType:type]) {
      if (![jsonElement isKindOfClass:NSNumber.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be primitive type!"];
      }
      object = jsonElement;
    } else if ([LcReflection isType:type KindOfClass:NSString.class]) {
      if (![jsonElement isKindOfClass:NSString.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be string!"];
      }
      object = jsonElement;
    } else if ([LcReflection isType:type KindOfClass:NSData.class]) {
      if (![jsonElement isKindOfClass:NSString.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be string!"];
      }
      object = [self.securityCenter base64Decode:jsonElement];
    } else if ([LcReflection isType:type KindOfClass:NSDictionary.class]) {
      if (![jsonElement isKindOfClass:NSDictionary.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be dictionary!"];
      }
      
      if (![genericTypes isKindOfClass:NSArray.class] ||
          genericTypes.count != 2 ||
          ![genericTypes[0] isKindOfClass:NSString.class] ||
          ![genericTypes[1] isKindOfClass:NSString.class]) {
        @throw [LcJsonException
                exceptionWithReason:
                @"generic types of dictionary should contain 2 string elements!"];
      }
      
      object = [NSMutableDictionary dictionary];
      for (__strong id key in jsonElement) {
        id value = jsonElement[key];
        
        if ([LcReflection isPrimitiveType:genericTypes[0]]) {
          key = [NSNumber numberWithLongLong:[key longLongValue]];
        }
        id keyObject = [self fromJsonElement:key
                                      toType:genericTypes[0]
                            withGenericTypes:nil
                             skipTypeAdapter:NO];
        id valueObject = [self fromJsonElement:value
                                        toType:genericTypes[1]
                              withGenericTypes:nil
                               skipTypeAdapter:NO];
        [object setValue:valueObject forKey:keyObject];
      }
    } else if ([LcReflection isType:type KindOfClass:NSArray.class] ||
               [LcReflection isType:type KindOfClass:NSSet.class] ||
               [LcReflection isType:type KindOfClass:NSOrderedSet.class]) {
      if (![jsonElement isKindOfClass:NSArray.class] &&
          ![jsonElement isKindOfClass:NSSet.class] &&
          ![jsonElement isKindOfClass:NSOrderedSet.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be array or set!"];
      }
      
      if (![genericTypes isKindOfClass:NSArray.class] ||
          genericTypes.count != 1 ||
          ![genericTypes[0] isKindOfClass:NSString.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"generic types of array "
                @"or set should contain 1 string element!"];
      }
      
      if ([LcReflection isType:type KindOfClass:NSArray.class]) {
        object = [NSMutableArray array];
      } else if ([LcReflection isType:type KindOfClass:NSSet.class]) {
        object = [NSMutableSet set];
      } else if ([LcReflection isType:type KindOfClass:NSOrderedSet.class]) {
        object = [NSMutableOrderedSet orderedSet];
      }
      
      for (id element in jsonElement) {
        NSString* elementType = NSStringFromClass([element class]);
        if ([LcReflection isType:elementType KindOfClass:NSArray.class] ||
            [LcReflection isType:elementType KindOfClass:NSSet.class] ||
            [LcReflection isType:elementType KindOfClass:NSOrderedSet.class]) {
          id elementObject = [self fromJsonElement:element
                                            toType:elementType
                                  withGenericTypes:genericTypes
                                   skipTypeAdapter:NO];
          [object addObject:elementObject];
        } else {
          id elementObject = [self fromJsonElement:element
                                            toType:genericTypes[0]
                                  withGenericTypes:nil
                                   skipTypeAdapter:NO];
          [object addObject:elementObject];
        }
      }
    } else {
      if (![jsonElement isKindOfClass:NSDictionary.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be dictionary!"];
      }
      Class clazz = NSClassFromString(type);
      if (clazz == NULL) {
        @throw [LcJsonException
                exceptionWithReason:
                [NSString
                 stringWithFormat:@"not supported type found, type=%@", type]];
      }
      object = [[clazz alloc] init];
      NSDictionary* properties = [LcReflection propertiesOfClass:clazz];
      for (NSString* propertyName in properties) {
        NSArray* genericTypes = nil;
        NSString* propertyType = properties[propertyName];
        if ([LcReflection isType:propertyType KindOfClass:NSArray.class] ||
            [LcReflection isType:propertyType KindOfClass:NSSet.class] ||
            [LcReflection isType:propertyType KindOfClass:NSOrderedSet.class]) {
          genericTypes = [self genericTypesOfPropertyName:propertyName
                                                  inClass:clazz
                                        withExpectedCount:1];
        } else if ([LcReflection isType:propertyType
                            KindOfClass:NSDictionary.class]) {
          genericTypes = [self genericTypesOfPropertyName:propertyName
                                                  inClass:clazz
                                        withExpectedCount:2];
        }
        id propertyJson = jsonElement[propertyName];
        id propertyObject = [self fromJsonElement:propertyJson
                                           toType:propertyType
                                 withGenericTypes:genericTypes
                                  skipTypeAdapter:NO];
        if (propertyObject == [NSNull null]) {
          propertyObject = nil;
        }
        [object setValue:propertyObject forKey:propertyName];
      }
    }
    
    return object;
  } @finally {
    if (!isAlreadyJsoning) {
      [self finishJsoning];
    }
  }
}

- (NSData*)toJson:(id)object {
  return [self toJson:object ofClass:[object class] withGenericTypes:nil];
}

- (NSData*)toJson:(id)object
          ofClass:(Class)clazz
 withGenericTypes:(NSArray*)genericTypes {
  LC_CHECK_NOT_NULL(clazz);
  @try {
    [self startJsoning];
    
    if (object == nil) {
      return nil;
    }
    
    id jsonElement = [self toJsonElement:object
                          ofDeclaredType:NSStringFromClass(clazz)
                        withGenericTypes:genericTypes
                         skipTypeAdapter:NO];
    if ([LcReflection isType:NSStringFromClass([jsonElement class])
                 KindOfClass:NSString.class]) {
      return [jsonElement toData];
    }
    NSError* error = nil;
    NSData* data = [NSJSONSerialization dataWithJSONObject:jsonElement
                                                   options:kNilOptions
                                                     error:&error];
    if (error != nil || data == nil) {
      @throw [LcJsonException
              exceptionWithReason:[NSString
                                   stringWithFormat:@"NSJSONSerialization "
                                   @"serialization exception, "
                                   @"error=%@!",
                                   error.localizedDescription]];
    }
    return data;
  } @finally {
    [self finishJsoning];
  }
}

- (id)toJsonElement:(id)object
     ofDeclaredType:(NSString*)declaredType
   withGenericTypes:(NSArray*)genericTypes
    skipTypeAdapter:(BOOL)skipTypeAdapter {
  BOOL isAlreadyJsoning = self.isJsoning;
  @try {
    if (!isAlreadyJsoning) {
      [self startJsoning];
    }
    if (object == nil || object == [NSNull null]) {
      return object;
    }
    
    if (!skipTypeAdapter) {
      id<LcTypeAdapter> typeAdapter = [self typeAdapterForType:declaredType];
      if (typeAdapter != nil) {
        return [typeAdapter serialize:object
                       ofDeclaredType:declaredType
                           withLcJson:self];
      }
    }
    
    id jsonElement = nil;
    if ([LcReflection isPrimitiveType:declaredType]) {
      if (![object isKindOfClass:NSNumber.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be primitive type"];
      }
      NSNumber* number = (NSNumber*)object;
      if ([declaredType isEqualToString:@"BOOL"]) {
        jsonElement = number.boolValue ? @YES : @NO;
      } else {
        jsonElement = number;
      }
    } else if ([LcReflection isType:declaredType KindOfClass:NSString.class]) {
      if (![object isKindOfClass:NSString.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be string"];
      }
      jsonElement = object;
    } else if ([LcReflection isType:declaredType KindOfClass:NSData.class]) {
      if (![object isKindOfClass:NSData.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be data"];
      }
      jsonElement = [self.securityCenter base64Encode:object];
    } else if ([LcReflection isType:declaredType
                        KindOfClass:NSDictionary.class]) {
      if (![object isKindOfClass:NSDictionary.class]) {
        @throw [LcJsonException
                exceptionWithReason:@"type mismatch, should be dictionary"];
      }
      
      if (![genericTypes isKindOfClass:NSArray.class] ||
          genericTypes.count != 2 ||
          ![genericTypes[0] isKindOfClass:NSString.class] ||
          ![genericTypes[1] isKindOfClass:NSString.class]) {
        @throw [LcJsonException
                exceptionWithReason:
                @"generic types of dictionary should contain 2 string elements!"];
      }
      
      jsonElement = [NSMutableDictionary dictionary];
      for (id key in object) {
        id value = object[key];
        id keyJson = [self toJsonElement:key
                          ofDeclaredType:genericTypes[0]
                        withGenericTypes:nil
                         skipTypeAdapter:NO];
        id valueJson = [self toJsonElement:value
                            ofDeclaredType:genericTypes[1]
                          withGenericTypes:nil
                           skipTypeAdapter:NO];
        if ([keyJson isKindOfClass:NSNumber.class]) {
          keyJson = [keyJson stringValue];
        }
        if (![keyJson isKindOfClass:NSString.class]) {
          @throw [LcJsonException
                  exceptionWithReason:
                  [NSString stringWithFormat:@"dictionary's key should be "
                   @"adaptered to string, key type is "
                   @"%@",
                   NSStringFromClass([keyJson class])]];
        }
        [jsonElement setValue:valueJson forKey:keyJson];
      }
    } else if ([LcReflection isType:declaredType KindOfClass:NSArray.class] ||
               [LcReflection isType:declaredType KindOfClass:NSSet.class] ||
               [LcReflection isType:declaredType
                        KindOfClass:NSOrderedSet.class]) {
                 if (![object isKindOfClass:NSArray.class] &&
                     ![object isKindOfClass:NSSet.class] &&
                     ![object isKindOfClass:NSOrderedSet.class]) {
                   @throw [LcJsonException
                           exceptionWithReason:@"type mismatch, should be array or set!"];
                 }
                 
                 if (![genericTypes isKindOfClass:NSArray.class] ||
                     genericTypes.count != 1 ||
                     ![genericTypes[0] isKindOfClass:NSString.class]) {
                   @throw [LcJsonException
                           exceptionWithReason:@"generic types of array "
                           @"or set should contain 1 string element!"];
                 }
                 
                 jsonElement = [NSMutableArray array];
                 for (id element in object) {
                   NSString* elementType = NSStringFromClass([element class]);
                   if ([LcReflection isType:elementType KindOfClass:NSArray.class] ||
                       [LcReflection isType:elementType KindOfClass:NSSet.class] ||
                       [LcReflection isType:elementType KindOfClass:NSOrderedSet.class]) {
                     id elementJson = [self toJsonElement:element
                                           ofDeclaredType:elementType
                                         withGenericTypes:genericTypes
                                          skipTypeAdapter:NO];
                     [jsonElement addObject:elementJson];
                   } else {
                     id elementJson = [self toJsonElement:element
                                           ofDeclaredType:genericTypes[0]
                                         withGenericTypes:nil
                                          skipTypeAdapter:NO];
                     [jsonElement addObject:elementJson];
                   }
                 }
               } else {
                 Class clazz = NSClassFromString(declaredType);
                 if (clazz == NULL) {
                   @throw [LcJsonException
                           exceptionWithReason:
                           [NSString stringWithFormat:@"not supported type found, type=%@",
                            declaredType]];
                 }
                 jsonElement = [NSMutableDictionary dictionary];
                 NSDictionary* properties = [LcReflection propertiesOfClass:[object class]];
                 for (NSString* propertyName in properties) {
                   NSArray* genericTypes = nil;
                   NSString* propertyType = properties[propertyName];
                   if ([LcReflection isType:propertyType KindOfClass:NSArray.class] ||
                       [LcReflection isType:propertyType KindOfClass:NSSet.class] ||
                       [LcReflection isType:propertyType KindOfClass:NSOrderedSet.class]) {
                     genericTypes = [self genericTypesOfPropertyName:propertyName
                                                             inClass:clazz
                                                   withExpectedCount:1];
                   } else if ([LcReflection isType:propertyType
                                       KindOfClass:NSDictionary.class]) {
                     genericTypes = [self genericTypesOfPropertyName:propertyName
                                                             inClass:clazz
                                                   withExpectedCount:2];
                   }
                   id propertyValue = [object valueForKey:propertyName];
                   id propertyJson = [self toJsonElement:propertyValue
                                          ofDeclaredType:propertyType
                                        withGenericTypes:genericTypes
                                         skipTypeAdapter:NO];
                   if (propertyJson != nil) {
                     if ([propertyJson isKindOfClass:NSNumber.class]) {
                       NSNumber* number = propertyJson;
                       if (number.doubleValue != 0) {
                         [jsonElement setObject:propertyJson forKey:propertyName];
                       }
                     } else {
                       [jsonElement setObject:propertyJson forKey:propertyName];
                     }
                   }
                 }
               }
    
    return jsonElement;
  } @finally {
    if (!isAlreadyJsoning) {
      [self finishJsoning];
    }
  }
}

- (BOOL)jsonEquals:(id)object1 with:(id)object2 {
  if (object1 == object2) {
    return YES;
  }
  if (object1 == nil || object2 == nil) {
    return NO;
  }
  if ([object1 isKindOfClass:NSString.class]) {
    if ([object2 isKindOfClass:NSString.class]) {
      return [object1 isEqualToString:object2];
    } else {
      return NO;
    }
  }
  if ([object1 class] != [object2 class]) {
    return NO;
  }

  NSData* jsonData1 = [self toJson:object1];
  NSData* jsonData2 = [self toJson:object2];
  return [jsonData1 isEqualToData:jsonData2];
}

- (id)jsonCopy:(id)object {
  if (object == nil) {
    return nil;
  }
  NSString* type = NSStringFromClass([object class]);
  id jsonElement = [self toJsonElement:object
                        ofDeclaredType:type
                      withGenericTypes:nil
                       skipTypeAdapter:NO];
  id copied = [self fromJsonElement:jsonElement
                             toType:type
                   withGenericTypes:nil
                    skipTypeAdapter:NO];
#ifdef DEBUG
// jsonEquals: not work for dictionary objects
// LC_CHECK_TRUE([self jsonEquals:object with:copied]);
#endif
  return copied;
}

#pragma mark -

+ (NSString*)typeNameOfProtocol:(Protocol*)protocol {
  return [NSString stringWithFormat:@"<%@>", NSStringFromProtocol(protocol)];
}

- (NSArray*)genericTypesOfPropertyName:(NSString*)propertyName
                               inClass:(Class)clazz
                     withExpectedCount:(int)expectedCount {
  Method method =
      class_getClassMethod(clazz, @selector(genericTypesOfProperty:));
  if (method == NULL) {
    @throw [LcJsonException
        exceptionWithReason:
            [NSString stringWithFormat:@"method genericTypesOfProperty: "
                                       @"of class %@ is not implemented",
                                       NSStringFromClass(clazz)]];
  }

  NSArray* genericTypes = [clazz genericTypesOfProperty:propertyName];
  if (genericTypes == nil) {
    @throw [LcJsonException
        exceptionWithReason:[NSString
                                stringWithFormat:@"property %@ of class %@ "
                                                 @"should return generic types",
                                                 propertyName,
                                                 NSStringFromClass(clazz)]];
  }
  for (id genericType in genericTypes) {
    if (![genericType isKindOfClass:NSString.class]) {
      @throw [LcJsonException
          exceptionWithReason:
              [NSString stringWithFormat:
                            @"generic types should only contains strings,"
                            @"property=%@, "
                            @"class=%@",
                            propertyName, NSStringFromClass(clazz)]];
    }
  }
  if (genericTypes.count != expectedCount) {
    @throw [LcJsonException
        exceptionWithReason:
            [NSString stringWithFormat:@"size of generic types of array type "
                                       @"should be %d, property=%@, "
                                       @"class=%@",
                                       expectedCount, propertyName,
                                       NSStringFromClass(clazz)]];
  }
  return genericTypes;
}

- (id<LcTypeAdapter>)typeAdapterForType:(NSString*)type {
  id<LcTypeAdapter> typeAdapter = self.typeAdapters[type];
  if (typeAdapter == nil && [LcReflection isProtocol:type]) {
    typeAdapter = self.defaultSubclassAdapter;
  }
  return typeAdapter;
}

- (void)startJsoning {
  LC_CHECK_FALSE(self.isJsoning);
  [self.jsoningThreads addObject:[NSThread currentThread]];
}

- (void)finishJsoning {
  LC_CHECK_TRUE(self.isJsoning);
  [self.jsoningThreads removeObject:[NSThread currentThread]];
}

- (BOOL)isJsoning {
  return [self.jsoningThreads containsObject:[NSThread currentThread]];
}

@end
